<?php

namespace mg\crypto;

interface KeySpec {
}

class PBEKeySpec implements KeySpec {

    private $password = null;
    private $salt = null;
    private $iterationCount = null;
    private $keyLength = null;

    public function __construct($password, $salt, $iterationCount, $keyLength) {
        $this->password = $password;
        $this->salt = $salt;
        $this->iterationCount = $iterationCount;
        $this->keyLength = $keyLength;
    }

    public function getIterationCount() {
        return $this->iterationCount;
    }

    public function getPassword() {
        return $this->password;
    }

    public function getSalt() {
        return $this->salt;
    }

    public function getKeyLength() {
        return $this->keyLength;
    }

}

interface SecretKey {
    public function getEncoded();
}


/**
 * Password based key deriviation function
 * http://tools.ietf.org/html/rfc2898#section-5.2
 *
 * Based on Henry Merriam's work
 * http://php.net/manual/en/function.hash-hmac.php#92684
 */
class PBKDF2 implements SecretKey {

    private $encoded = null;

    public function __construct(PBEKeySpec $spec) {
        $password = $spec->getPassword();
        $salt = $spec->getSalt();
        $iterationCount = $spec->getIterationCount();
        $keyLength = $spec->getKeyLength();

        $hashLength = 64; // 64 bytes

        // $hmac = hash_init($algorithm, HASH_HMAC, $salt);

        // 1. If dkLen > (2^32 - 1) * hLen, output "derived key too long" and
        // stop.
        if ($keyLength <= 0xffffffff * $hashLength) {
            // 2. Let l be the number of hLen-octet blocks in the derived key,
            // rounding up, and let r be the number of octets in the last
            // block:
            // l = CEIL (dkLen / hLen) ,
            // r = dkLen - (l - 1) * hLen .

            $length = ceil($keyLength / $hashLength); // length in octet blocks

            $derivedKey = null;

            // 3. For each block of the derived key apply the function F defined
            // below to the password P, the salt S, the iteration count c, and
            // the block index to compute the block:

            for ($c = 1; $c <= $length; $c++) {
                // U_1 = PRF (P, S || INT (i)) ,
                $u = hash_hmac('whirlpool', $salt . pack('N', $c), $password, true);
                $block = $u;
                for ($j = 1; $j < $iterationCount; $j++) {
                    // U_c = PRF (P, U_{c-1}
                    $u = hash_hmac('whirlpool', $u, $password, true);
                    	
                    // F (P, S, c, i) = U_1 \xor U_2 \xor ... \xor U_c
                    $block ^= $u;
                }
                // DK = T_1 || T_2 ||  ...  || T_l<0..r-1>
                $derivedKey .= $block;
            }

            $this->encoded = substr($derivedKey, 0, $keyLength);
        }
    }

    public function getEncoded() {
        return $this->encoded;
    }
}

final class SecretKeyFactory {

    private static $instance = null;

    private function __construct() {
    }

    private function __clone() {
    }

    /**
     * @param KeySpec $spec
     *
     * @return SecretKey $key
     */
    public function generateSecret(KeySpec $spec) {
        if($spec instanceof PBEKeySpec) {
            /* @var $spec PBEKeySpec */
            $key = new PBKDF2($spec);
        }

        return $key;
    }

    public static function resolveInstance() {
        if(self :: $instance === null) {
            self :: $instance = new SecretKeyFactory();
        }

        return self :: $instance;
    }
}

final class Secure {

    /**
     * Create a password based key
     *
     * @param $password
     * @param $salt
     * @param $iterationCount
     * @param $keyLength The length of the resulting key, in octets
     */
    public static function pbk($password, $salt, $iterationCount = 1024, $keyLength = 64) {
        return in(function(SecretKeyFactory $factory) use($password, $salt, $iterationCount, $keyLength) {
            $spec = new PBEKeySpec($password, $salt, $iterationCount, $keyLength);

            return bin2hex($factory->generateSecret($spec)->getEncoded());
        });
    }

}

